package jp.wug.core;

import com.google.inject.TypeLiteral;
import jp.wug.Renderer;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

class SimpleModelFactory implements ModelFactory {
    private static final Method RENDERER_RENDER_METHOD;

    static {
        try {
            RENDERER_RENDER_METHOD = Renderer.class.getMethod("render", Object.class, Object.class);
        } catch (NoSuchMethodException e) {
            throw new AssertionError(e);
        }
    }

    private final TypeLiteral<?> type;
//    private final Map<CallKey<?, ?>, ModelFactory> children = new HashMap<CallKey<?, ?>, ModelFactory>();

    SimpleModelFactory(TypeLiteral<?> type) {
        Class<?> rawType = type.getRawType();
        if (!Modifier.isPublic(rawType.getModifiers())) {
            throw new RuntimeException(rawType + " must be public.");
        }

        this.type = type;
    }

    public TypeLiteral<?> getType() {
        return type;
    }

//    public ModelFactory getChild(CallKey<?, ?> key) {
//        if (!children.containsKey(key)) {
//            throw new IllegalArgumentException("Bad key for children: " + key);
//        }
//        return children.get(key);
//    }

    private static class Call<T, U> {
        private final String key;
        private final Method method;
        private final Translators.Translator<T, ?> inputTranslator;
        private final Translators.Translator<?, U> outputTranslator;
        private final boolean skipIfInputIsNull;

        private Call(String key, Method method, Translators.Translator<T, ?> inputTranslator, Translators.Translator<?, U> outputTranslator, boolean skipIfInputIsNull) {
            this.key = key;
            this.method = method;
            this.inputTranslator = inputTranslator;
            this.outputTranslator = outputTranslator;
            this.skipIfInputIsNull = skipIfInputIsNull;
        }

        public String getKey() {
            return key;
        }

        U call(Model model, T t) throws InvocationTargetException {
            Object input = null;
            if (inputTranslator != null) {
                input = inputTranslator.translate(t);
                if (skipIfInputIsNull && input == null) {
                    return outputTranslator.translate(null);
                }
            }
            Object result;
            try {
                Object target = model.getValue();
                if (inputTranslator == null) {
                    result = method.invoke(target);
                } else {
                    result = method.invoke(target, input);
                }
            } catch (IllegalArgumentException e) {
                throw new AssertionError(e);
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            }
            return uncheckedTranslate(outputTranslator, result);
        }

        @SuppressWarnings("unchecked")
        private <A, B> B uncheckedTranslate(Translators.Translator<A, B> translator, Object object) {
            return translator.translate((A) object);
        }

        static <T, U> Call<T, U> createModelGetter(String key, TypeLiteral<?> type, Method method, TypeLiteral<T> input, TypeLiteral<U> output, Collection<ModelFactory> modelFactories) {
            Translators.Translator<?, U> outputTranslator = Translators.TRANSLATORS.get(type.getReturnType(method), output, modelFactories);
            Call<T, U> call;
            if (input.equals(TypeLiteral.get(void.class))) {
                call = new Call<T, U>(key, method, null, outputTranslator, false);
            } else {
                List<TypeLiteral<?>> methodInputList = type.getParameterTypes(method);
                if (methodInputList.size() != 1) {
                    throw new AssertionError(method + " must have 1 argument.");
                }
                Translators.Translator<T, ?> inputTranslator = Translators.TRANSLATORS.get(input, methodInputList.get(0), modelFactories);
                call = new Call<T, U>(key, method, inputTranslator, outputTranslator, Call.isArgumentPrimitive(type, method));
            }
            return call;
        }

        private static boolean isArgumentPrimitive(TypeLiteral<?> type, Member member) {
            List<TypeLiteral<?>> parameterTypes = type.getParameterTypes(member);
            return parameterTypes.size() == 1 && parameterTypes.get(0).getRawType().isPrimitive();
        }
    }

    private static class CallKey<T, U> {
        private final String key;
        private final TypeLiteral<T> inputType;
        private final TypeLiteral<U> outputType;

        CallKey(String key, TypeLiteral<T> inputType, TypeLiteral<U> outputType) {
            this.key = key;
            this.inputType = inputType;
            this.outputType = outputType;
        }

        public String getKey() {
            return key;
        }

        @Override
        public String toString() {
            return key + ": <" + inputType + ", " + outputType + ">";
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof CallKey<?, ?>) {
                CallKey<?, ?> k = (CallKey<?, ?>) obj;
                return key.equals(k.key) && inputType.equals(k.inputType) && outputType.equals(k.outputType);
            }
            return super.equals(obj);
        }

        @Override
        public int hashCode() {
            return toString().hashCode();
        }
    }


    private class ModelImpl implements Model {
        private final ModelFactory modelFactory;
        private final Object object;

        private ModelImpl(ModelFactory modelFactory, Object object) {
            if (object == null) {
                throw new IllegalArgumentException("Model objects cannot be null.");
            }
            this.modelFactory = modelFactory;
            this.object = object;
        }

        public ModelFactory getModelFactory() {
            return modelFactory;
        }

        public Object getValue() {
            return object;
        }
    }

    private String prefixed(String prefix, String name) {
        return prefix + name.substring(0, 1).toUpperCase() + name.substring(1);
    }

    public Model create(Object object) {
        return new ModelImpl(this, type.getRawType().cast(object));
    }

    private static class ExpressionImpl<T, U> implements Expression<T, U> {
        private final ModelFactory parent;
        private final ModelFactory modelFactory;
        private final Call<T, U> call;

        ExpressionImpl(ModelFactory parent, Call<T, U> call, ModelFactory modelFactory) {
            this.parent = parent;
            this.call = call;
            this.modelFactory = modelFactory;
        }

        public U call(Model model, T t) {
            try {
                ModelImpl modelImpl = (ModelImpl) model;
                if (modelImpl.getModelFactory() != parent) {
                    throw new AssertionError();
                }
                return call.call(model, t);
            } catch (InvocationTargetException e) {
                Throwable targetException = e.getTargetException();
                if (targetException instanceof RuntimeException) {
                    throw (RuntimeException) targetException;
                }
                throw new AssertionError(e);
            }
        }

        public U tryCall(Model model, T t) throws ModelException {
            try {
                ModelImpl modelImpl = (ModelImpl) model;
                if (modelImpl.getModelFactory() != parent) {
                    throw new AssertionError();
                }
                return call.call(model, t);
            } catch (InvocationTargetException e) {
                throw new ModelException(e.getTargetException(), model.getValue(), (String[]) t, (String[]) t);
            } catch (NumberFormatException e) {
                throw new ModelException(e, model.getValue(), (String[]) t, (String[]) t);
            }
        }

        public ModelFactory getModelFactory() {
            return modelFactory;
        }

        public List<String> asStrings() {
            return Arrays.asList(call.getKey());
        }

        @Override
        public String toString() {
            return call.getKey() + ":";
        }
    }

    public <T, U> List<TypeLiteral<?>> getModelType(TypeLiteral<T> srcType, TypeLiteral<U> destType) {
        List<TypeLiteral<?>> list = new ArrayList<TypeLiteral<?>>();
        if (destType.equals(TypeLiteral.get(Model.class))) {
            list.add(srcType);
        }

        if (srcType.equals(new TypeLiteral<Renderer<String, Model>>() {
        })) {
            list.add(destType.getParameterTypes(SimpleModelFactory.RENDERER_RENDER_METHOD).get(1));
        }
        return list;
    }

    public <T, U> Expression<T, U> register(String name, TypeLiteral<T> inputType, TypeLiteral<U> outputType, ModelFactory child) throws NoSuchMethodException {
        return register0(name, inputType, outputType, child == null ? Arrays.<ModelFactory>asList() : Arrays.asList(child));
    }

    private <T, U> Expression<T, U> register0(String name, TypeLiteral<T> inputType, TypeLiteral<U> outputType, List<ModelFactory> child) throws NoSuchMethodException {
        String[] names;
        if (outputType.equals(TypeLiteral.get(boolean.class))) {
            names = new String[]{name, prefixed("get", name), prefixed("is", name)};
        } else if (!outputType.equals(TypeLiteral.get(void.class))) {
            names = new String[]{name, prefixed("get", name)};
        } else if (!inputType.equals(TypeLiteral.get(void.class))) {
            names = new String[]{name, prefixed("set", name)};
        } else {
            names = new String[]{name};
        }
        TypeLiteral[] parameterTypes;
        if (TypeLiteral.get(void.class).equals(inputType)) {
            parameterTypes = new TypeLiteral[0];
        } else {
            parameterTypes = new TypeLiteral[]{inputType};
        }
        Method method = findMethod(names, parameterTypes);

        Map<TypeLiteral<?>, ModelFactory> childCand = new HashMap<TypeLiteral<?>, ModelFactory>();
        for (ModelFactory m : child) {
            childCand.put(m.getType(), m);
        }
        for (TypeLiteral<?> t : getModelType(type.getReturnType(method), outputType)) {
            if (!childCand.containsKey(t)) {
                childCand.put(t, new SimpleModelFactory(t));
            }
        }
        for (TypeLiteral<?> parameterType : type.getParameterTypes(method)) {
            for (TypeLiteral<?> t : getModelType(inputType, parameterType)) {
                if (!childCand.containsKey(t)) {
                    childCand.put(t, new SimpleModelFactory(t));
                }
            }
        }

        CallKey<T, U> key = new CallKey<T, U>(name, inputType, outputType);
        Call<T, U> call = Call.createModelGetter(name, type, method, inputType, outputType, childCand.values());
        if (childCand.size() > 1) {
            throw new IllegalStateException();
        }
        Iterator<ModelFactory> iterator = childCand.values().iterator();
        return new ExpressionImpl<T, U>(this, call, iterator.hasNext() ? iterator.next() : null);
    }

    private Method findMethod(String[] names, TypeLiteral<?>[]... args) throws NoSuchMethodException {
        for (Method method : type.getRawType().getMethods()) {
            for (String name : names) {
                if (method.getName().equals(name)) {
                    List<TypeLiteral<?>> parameterTypes = type.getParameterTypes(method);
                    for (TypeLiteral<?>[] arg : args) {
                        if (arg.length == parameterTypes.size()) {
                            boolean failed = false;
                            for (int k = 0; k < arg.length; k++) {
                                if (!Translators.TRANSLATORS.contains(arg[k], parameterTypes.get(k))) {
                                    failed = true;
                                    break;
                                }
                            }
                            if (!failed) {
                                return method;
                            }
                        }
                    }
                }
            }
        }
        /*Just to throw NoSuchMethodException*/
        Class[] argClasses = new Class[args[0].length];
        for (int i = 0; i < argClasses.length; i++) {
            argClasses[i] = args[0][i].getRawType();
        }
        return type.getRawType().getMethod(names[0], argClasses);
    }
}